← Writeups

Authentication bypass via OAuth implicit flow

Bypass de autenticación abusando del flujo OAuth implícito: la app cliente confía en el email enviado a /authenticate sin vincularlo al access token, permitiendo suplantar la identidad de carlos.

Target Overview & Vulnerability Summary

El laboratorio permite el login con una cuenta de red social vía un servicio OAuth. Una validación defectuosa por parte de la aplicación cliente permite a un atacante iniciar sesión en la cuenta de otro usuario sin conocer su password: el endpoint /authenticate confía en el email que el cliente envía y lo usa para crear la sesión, sin verificar que el access token corresponda realmente a ese email.

Campo Valor
Severidad Alta (account takeover: login en la cuenta de carlos sin credenciales)
Endpoint vulnerable POST /authenticate (creación de sesión), GET /auth (autorización OAuth)
Root cause El cliente crea la sesión con un email declarado por el atacante, sin validar que el access token pertenezca a esa identidad

Technical Breakdown & Exploitation Flow

Credenciales de laboratorio: wiener:peter. Email objetivo: carlos@carlos-montoya.net.

Flujo OAuth legítimo

Al iniciar sesión con las credenciales propias se sigue la secuencia:

  1. GET /my-account
  2. Redirección a GET /social-login
  3. GET /auth hacia el servidor OAuth con el grant implicit:
GET /auth?client_id=kf8dlbsnofv01tyhfe43n&redirect_uri=https://0a7400930475bcb8805d1705004a0003.web-security-academy.net/oauth-callback&response_type=token&nonce=-6836096&scope=openid%20profile%20email HTTP/2

Respuesta con la cookie de interacción del servidor OAuth:

Set-Cookie: _interaction=UYxHDxXsg5L3b1NMnMtEr;
  1. GET /interaction/UYxHDxXsg5L3b1NMnMtEr (con la cookie _interaction) presenta el formulario de login del proveedor OAuth.
  2. POST /interaction/UYxHDxXsg5L3b1NMnMtEr/login:
username=wiener&password=peter
  1. Consentimiento en GET /auth/UYxHDxXsg5L3b1NMnMtEr.

Semántica de los parámetros del flujo:

  • client_id: identifica la app cliente; se genera cuando la app se registra con el servicio OAuth.
  • redirect_uri: URI a la que el navegador es redirigido al finalizar, donde se entrega la respuesta a la app cliente (CALLBACK URI / CALLBACK ENDPOINT).
  • response_type: determina el tipo de respuesta esperada. Con valor token, se usa el grant type implícito (el token se entrega directamente en el fragment de la URL, sin code exchange).
  • state (OAuth 2.0): valor único e inadivinable enlazado a la sesión de la app cliente; el proveedor debe devolverlo junto al code, funcionando como token CSRF para proteger el callback.
  • nonce (OpenID Connect): enlaza el token con el cliente y sirve como parámetro de validación del token (anti-replay del id_token).
  • scope: especifica qué subconjunto de datos del usuario solicita la app cliente.

Al conceder el acceso, el navegador es redirigido a una URL cuyo fragment contiene el access_token, extraído por JavaScript del lado cliente:

Redirecting to https://0ae00012030597c3814e2f9700440021.web-security-academy.net/oauth-callback#access_token=bY66yHIhrP95bFDWfJZsFVJSzHzPyYwU3pgCMKCe53X&expires_in=3600&token_type=Bearer&scope=openid%20profile%20email.

El endpoint vulnerable /authenticate

El navegador envía el token extraído del fragment al endpoint de la app cliente:

POST /authenticate HTTP/2
{"email":"wiener@hotdog.com","username":"wiener","token":"bY66yHIhrP95bFDWfJZsFVJSzHzPyYwU3pgCMKCe53X"}

RESPONSE:
Set-Cookie: session=03iJIj8DY2kTWZg0m2oJ4sOjA9qtd5Gz;

La aplicación necesita un mecanismo para almacenar las sesiones; en este caso no usa passwords. Al cerrar sesión y volver a autenticarse, el cliente vuelve a emitir un POST /authenticate. Este request es manipulable: el campo email se puede cambiar a carlos@carlos-montoya.net y, como la app confía en ese valor para crear la sesión, se obtiene la sesión implícitamente confiada de carlos.

  1. Completar el flujo OAuth legítimo con wiener:peter para obtener un access_token válido.
  2. Interceptar el POST /authenticate y reemplazar email por carlos@carlos-montoya.net.
  3. La respuesta entrega Set-Cookie: session=... con la sesión de carlos. El laboratorio se resuelve con el login en la cuenta de carlos (su email es carlos@carlos-montoya.net).

Root Cause Analysis

En este escenario, POST /authenticate equivale a completar un formulario usuario/password, pero no existe ningún secreto que comparar: la identidad se confía de forma implícita porque la app cliente no valida la relación entre el access token y el email declarado. El patrón defectuoso es usar el email del body como fuente de identidad sin verificar la identidad real del titular del token (p. ej. consultando el endpoint userinfo del proveedor con el token):

// defectuoso: la identidad la define el campo email del cliente
var req struct{ Email, Username, Token string }
json.NewDecoder(r.Body).Decode(&req)

if tokenStore.Valid(req.Token) {        // solo valida que el token exista
    session.User = lookupByEmail(req.Email) // pero no que el token pertenezca a ese email
}

Además, el grant type implicit entrega el access token en el fragment de la URL (response_type=token), quedando expuesto a historial, logs de referer y extensión, y sin el code intercambiable que sí permitiría validar la identidad server-side.

Secure Remediation

  • Vincular el token a la identidad: usar el access token para consultar userinfo del proveedor (GET /userinfo con Authorization: Bearer <token>) y derivar el email/sub desde esa respuesta, nunca desde el body del cliente:
identity, err := oauth.UserInfo(req.Token) // sub/email resueltos server-side
if err != nil {
    return http.StatusUnauthorized
}
session.User = identity.Email
  • Migrar al authorization code flow (response_type=code + PKCE): el code se intercambia server-side y el token nunca transita por el navegador; el servidor puede validar aud, iss y nonce.
  • Validar nonce y state en el callback, y confirmar que el token fue emitido para este client_id.
  • Almacenar el token enlazado a la sesión y a la identidad verificada, nunca confiar en campos de identidad del body.